値の分布に偏りがあるフィールドに対してフィールドインデックスを設定した場合の動作を検証してみた

値の分布に偏りがあるフィールドに対してフィールドインデックスを設定した場合の動作を検証してみた

Clock Icon2025.01.05

リテールアプリ共創部@大阪の岩田です。

2024/11/21付けのアップデートによってCW Logs Insightsのクエリでフィールドインデックスが利用可能になりました。この機能を活用することで、クエリによるスキャン量低減とコストの最適化が実現できます。

https://aws.amazon.com/about-aws/whats-new/2024/11/amazon-cloudwatch-logs-field-indexes-log-group-selection-log-insights/

フィールドインデックスを設定する対象としてはカーディナリティの高いフィールドが推奨されており、公式ドキュメントには以下のように記載されています。

Fields that have high cardinality of values are also good candidates for field indexes because a query using these field indexes will complete faster because it limits the log events that are being matched to the target value.

https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CloudWatchLogs-Field-Indexing.html

フィールドインデックスの内部構造の詳細は明かされていませんが、この辺の考え方は一般的なRDBにおけるインデックスと同じ考え方になりそうですね。ここで気になったのが、カーディナリティの低いフィールドに対してフィールドインデックスを設定した場合の動作がどうなるか?という点です。

RDBでもカーディナリティの低い列に対するインデックスは推奨されませんが、これは値の分布が均一であるという前提にもとづいています。例えば'0'と'1'しか取り得ないisRareというカラムがあったとして、'0'と'1'が99.999:0.001の割合で分布しているとします。この例ではSELECT ...略 WHERE isRare = '0'というクエリはインデックスを利用すると逆に効率が悪くなるため実行計画としてはスキャンが採用されるはずです。対してSELECT ...略 WHERE isRare = '1'というクエリはインデックスを利用することで効率的にレコードが取得できるため、実行計画はインデックスを用いたものが採用されるはずです。

このように値の分布が偏ることが明白なケースではカーディナリティの低いカラムであってもインデックスを貼ることがあります。

※DBMS側が対応していれば部分インデックスを使うとより効率的です。

上記のようにカーディナリティは低いものの値の分布に偏りがあるフィールドに対してフィールドインデックスを設定した場合にCW Logs Insightsのクエリがフィールドインデックスを利用してくれるのか気になったので実際に検証してみました。

以後はすべてフィールドインデックスがBツリーもしくはB+ツリー構造という仮定に基づいて考察していきます。

やってみる

それでは実際に検証してみます。

ロググループの作成

まず以下のCFnテンプレートでCW Logsのロググループとログストリームを作成します。

AWSTemplateFormatVersion: 2010-09-09
Resources:
  HasRareAttributeLogGroup:
    Type: 'AWS::Logs::LogGroup'
    DeletionPolicy: Delete
    Properties:
      RetentionInDays: 7
      LogGroupName: HasRareAttribute
      FieldIndexPolicies:
        - Fields:
            - isRare
  HasRareAttributeLogStream:
    Type: AWS::Logs::LogStream
    Properties:
      LogGroupName: !Ref HasRareAttributeLogGroup
      LogStreamName: default

上記テンプレートを使ってスタックをデプロイします。

aws cloudformation create-stack --stack-name fld-index-test --template-body  file://template.yaml

デプロイされたロググループの定義は以下の通りです。

デプロイされたロググループの定義

フィールドインデックスポリシーでisRareというフィールドに対してフィールドインデックスが定義されています。

ログの書き込み

続いて以下のPythonスクリプトで上記ロググループに対してログを書き込みます。

import json
import time
import boto3

client = boto3.client('logs')
for i in range(10):
    log_events = []
    for j in range(10000):

        if j == 0:
            is_rare = "1"
        else:
            is_rare = "0"

        log_event = {
            'timestamp': int(time.time()) * 1000,
            'message': json.dumps({'msg': f'{i}-{j}', 'isRare': is_rare})
        }
        log_events.append(log_event)
    res = client.put_log_events(logGroupName='HasRareAttribute',logStreamName='default',logEvents=log_events)
    print(res)

1万件に1件の割合でisRareというフィールドが1になるようにログを生成し、合計10万件のログを書き込んでいます。スクリプトの実行完了後に意図通りログが書き込めているか以下のクエリで確認してみましょう。

stats count(isRare) by isRare

結果は以下の通りでした。

ログ書き込みの確認結果

isRare1のログが10件、isRare0のログが99,990件書き込まれています。

分布に偏りのあるフィールドを指定したクエリを実行

ログの準備ができたのでフィルター条件にisRareを指定してクエリを実行してみます。まずはisRare0のログをクエリしてみます。ほとんどのログはisRareの値が0なのでインデックスを利用すると逆に効率が悪くなるはずです。

filter isRare = '0'

結果は以下の通りでした。

isRareが0のログをクエリした結果

99,990 の 10000 の一致したレコードの表示
100,000 レコード (6.0 MB) が 2.6s @ 38,491 records/s (2.3 MB/s) でスキャンされました

と出力されており、フィールドインデックスが利用されていないことが分かります。

続いてisRare1のログをクエリしてみます。isRare1のログはは10件だけなのでフィールドインデックスを利用すると効率よくクエリできるはずです。

filter isRare = '1'

結果は以下の通りでした。

isRareが1のログをクエリした結果

10 の 10 の一致したレコードの表示
100,000 レコード (6.0 MB) が 1.2s @ 84,674 records/s (5.1 MB/s) でスキャンされました

と出力されており、こちらのパターンでもフィールドインデックスは利用されませんでした。

まとめ

この後ログの量を増やしたり、分布の割合を微調整したり、色々試したのですが今回の検証の範囲ではisRareに設定したフィールドインデックスが利用されることはありませんでした。統計情報のような概念が存在するのか?とか、仮に存在するのであればサンプリングレートはどの程度なのか?とか色々と妄想は膨らみますが、RDBにおけるBツリー/B+ツリーインデックスと同じような感覚でフィールドインデックスポリシーを設計するのは避けたほうが良さそうです。意図通りフィールドインデックスが利用されているか実際にクエリしながらテストするのが大事そうですね。

参考

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.